﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace BaseHangul
{
    /// <summary>
    /// Class for BaseHangul Encoding
    /// see http://api.dcmys.jp/basehangul/
    /// </summary>
    public static class BaseHangulEncoding
    {
        static string charTable = "가각간갇갈갉갊감갑값갓갔강갖갗같갚갛개객갠갤갬갭갯갰갱갸갹갼걀걋걍걔걘걜거걱건걷걸걺검겁것겄겅겆겉겊겋게겐겔겜겝겟겠겡겨격겪견겯결겸겹겻겼경곁계곈곌곕곗고곡곤곧골곪곬곯곰곱곳공곶과곽관괄괆괌괍괏광괘괜괠괩괬괭괴괵괸괼굄굅굇굉교굔굘굡굣구국군굳굴굵굶굻굼굽굿궁궂궈궉권궐궜궝궤궷귀귁귄귈귐귑귓규균귤그극근귿글긁금급긋긍긔기긱긴긷길긺김깁깃깅깆깊까깍깎깐깔깖깜깝깟깠깡깥깨깩깬깰깸깹깻깼깽꺄꺅꺌꺼꺽꺾껀껄껌껍껏껐껑께껙껜껨껫껭껴껸껼꼇꼈꼍꼐꼬꼭꼰꼲꼴꼼꼽꼿꽁꽂꽃꽈꽉꽐꽜꽝꽤꽥꽹꾀꾄꾈꾐꾑꾕꾜꾸꾹꾼꿀꿇꿈꿉꿋꿍꿎꿔꿜꿨꿩꿰꿱꿴꿸뀀뀁뀄뀌뀐뀔뀜뀝뀨끄끅끈끊끌끎끓끔끕끗끙끝끼끽낀낄낌낍낏낑나낙낚난낟날낡낢남납낫났낭낮낯낱낳내낵낸낼냄냅냇냈냉냐냑냔냘냠냥너넉넋넌널넒넓넘넙넛넜넝넣네넥넨넬넴넵넷넸넹녀녁년녈념녑녔녕녘녜녠노녹논놀놂놈놉놋농높놓놔놘놜놨뇌뇐뇔뇜뇝뇟뇨뇩뇬뇰뇹뇻뇽누눅눈눋눌눔눕눗눙눠눴눼뉘뉜뉠뉨뉩뉴뉵뉼늄늅늉느늑는늘늙늚늠늡늣능늦늪늬늰늴니닉닌닐닒님닙닛닝닢다닥닦단닫달닭닮닯닳담답닷닸당닺닻닿대댁댄댈댐댑댓댔댕댜더덕덖던덛덜덞덟덤덥덧덩덫덮데덱덴델뎀뎁뎃뎄뎅뎌뎐뎔뎠뎡뎨뎬도독돈돋돌돎돐돔돕돗동돛돝돠돤돨돼됐되된될됨됩됫됴두둑둔둘둠둡둣둥둬뒀뒈뒝뒤뒨뒬뒵뒷뒹듀듄듈듐듕드득든듣들듦듬듭듯등듸디딕딘딛딜딤딥딧딨딩딪따딱딴딸땀땁땃땄땅땋때땍땐땔땜땝땟땠땡떠떡떤떨떪떫떰떱떳떴떵떻떼떽뗀뗄뗌뗍뗏뗐뗑뗘뗬또똑똔똘똥똬똴뙈뙤뙨뚜뚝뚠뚤뚫뚬뚱뛔뛰뛴뛸뜀뜁뜅뜨뜩뜬뜯뜰뜸뜹뜻띄띈띌띔띕띠띤띨띰띱띳띵라락란랄람랍랏랐랑랒랖랗래랙랜랠램랩랫랬랭랴략랸럇량러럭런럴럼럽럿렀렁렇레렉렌렐렘렙렛렝려력련렬렴렵렷렸령례롄롑롓로록론롤롬롭롯롱롸롼뢍뢨뢰뢴뢸룀룁룃룅료룐룔룝룟룡루룩룬룰룸룹룻룽뤄뤘뤠뤼뤽륀륄륌륏륑류륙륜률륨륩륫륭르륵른를름릅릇릉릊릍릎리릭린릴림립릿링마막만많맏말맑맒맘맙맛망맞맡맣매맥맨맬맴맵맷맸맹맺먀먁먈먕머먹먼멀멂멈멉멋멍멎멓메멕멘멜멤멥멧멨멩며멱면멸몃몄명몇몌모목몫몬몰몲몸몹못몽뫄뫈뫘뫙뫼묀묄묍묏묑묘묜묠묩묫무묵묶문묻물묽묾뭄뭅뭇뭉뭍뭏뭐뭔뭘뭡뭣뭬뮈뮌뮐뮤뮨뮬뮴뮷므믄믈믐믓미믹민믿밀밂밈밉밋밌밍및밑바박밖밗반받발밝밞밟밤밥밧방밭배백밴밸뱀뱁뱃뱄뱅뱉뱌뱍뱐뱝버벅번벋벌벎범법벗벙벚베벡벤벧벨벰벱벳벴벵벼벽변별볍볏볐병볕볘볜보복볶본볼봄봅봇봉봐봔봤봬뵀뵈뵉뵌뵐뵘뵙뵤뵨부북분붇불붉붊붐붑붓붕붙붚붜붤붰붸뷔뷕뷘뷜뷩뷰뷴뷸븀븃븅브븍븐블븜븝븟비빅빈빌빎빔빕빗";
        static char paddingChar = '흐';

        static string checkPattern = null;
        static string filterPattern = null;
        private static void PreparePattern()
        {
            if (checkPattern != null && filterPattern != null)
                return;
            checkPattern = string.Format("^[{0}{1}]*?$", charTable, paddingChar);
            filterPattern = string.Format("[^{0}{1}]", charTable, paddingChar);
        }

        static Dictionary<char, int> reverseTable = null;
        private static void InitializeReverseTable()
        {
            if (reverseTable != null)
                return;
            Dictionary<char, int> t = new Dictionary<char, int>();
            for (int i = 0; i < charTable.Length; ++i )
            {
                t[charTable[i]] = i;
            }
            reverseTable = t;
        }


        /// <summary>
        /// Encode given binary data to a string using BaseHangul encoding
        /// </summary>
        /// <param name="data">Binary data to encode</param>
        /// <returns>BaseHangul encoded string</returns>
        public static string Encode(byte[] data)
        {
            return Encode(data, 0, data.Length);
        }

        /// <summary>
        /// Encode given binary data to a string using BaseHangul encoding
        /// </summary>
        /// <param name="data">Array segement contains binary data to encode</param>
        /// <returns>BaseHangul encoded string</returns>
        public static string Encode(ArraySegment<byte> data)
        {
            return Encode(data.Array, data.Offset, data.Count);
        }

        /// <summary>
        /// Encode given binary data to a string using BaseHangul encoding
        /// </summary>
        /// <param name="data">Byte array buffer contains binary data to encode</param>
        /// <param name="startOffset">Start offset of data to encode. This function will encode data from <paramref name="startOffset"/> to the end of data buffer.</param>
        /// <returns>BaseHangul encoded string</returns>
        /// <exception cref="ArgumentException">Thrown when invalid offset was given</exception>
        public static string Encode(byte[] data, int startOffset)
        {
            return Encode(data, startOffset, data.Length - startOffset);
        }

        /// <summary>
        /// Encode given binary data to a string using BaseHangul encoding
        /// </summary>
        /// <param name="data">Byte array buffer contains binary data to encode</param>
        /// <param name="startOffset">Start offset of data to encode</param>
        /// <param name="length">Length of data to encode</param>
        /// <returns>BaseHangul encoded string</returns>
        /// <exception cref="ArgumentException">Thrown when invalid offset or length were given</exception>
        public static string Encode(byte[] data, int startOffset, int length)
        {
            if (startOffset < 0)
            {
                throw new ArgumentException("Invalid offset", "startOffset");
            }
            if (data.Length < startOffset + length)
            {
                throw new ArgumentException("Not enough data for given length to encode", "length");
            }

            StringBuilder resBuilder = new StringBuilder();
            long buf;
            char c0, c1, c2;
            int endOffset = startOffset + length;
            for (int i = startOffset + 5; i <= endOffset; i += 5)
            {
                buf = (((data[i - 5] * (long)0x100 + data[i - 4]) * (long)0x100 + data[i - 3]) * (long)0x100 + data[i - 2]) * (long)0x100 + data[i - 1];
                c0 = charTable[(int)(buf % 1024)]; buf /= 1024;
                c1 = charTable[(int)(buf % 1024)]; buf /= 1024;
                c2 = charTable[(int)(buf % 1024)]; buf /= 1024;
                resBuilder.Append(charTable[(int)buf]);
                resBuilder.Append(c2);
                resBuilder.Append(c1);
                resBuilder.Append(c0);
            }
            switch (length % 5)
            {
                case 1:
                    {
                        resBuilder.Append(charTable[data[endOffset - 1] * 4]);
                        resBuilder.Append(paddingChar);
                        resBuilder.Append(paddingChar);
                        resBuilder.Append(paddingChar);
                    }
                    break;

                case 2:
                    {
                        buf = (data[endOffset - 2] * (long)0x100 + data[endOffset - 1]) * 16;
                        c0 = charTable[(int)(buf % 1024)]; buf /= 1024;
                        resBuilder.Append(charTable[(int)buf]);
                        resBuilder.Append(c0);
                        resBuilder.Append(paddingChar);
                        resBuilder.Append(paddingChar);
                    }
                    break;

                case 3:
                    {
                        buf = ((data[endOffset - 3] * (long)0x100 + data[endOffset - 2]) * (long)0x100 + data[endOffset - 1]) * 64;
                        c0 = charTable[(int)(buf % 1024)]; buf /= 1024;
                        c1 = charTable[(int)(buf % 1024)]; buf /= 1024;
                        resBuilder.Append(charTable[(int)buf]);
                        resBuilder.Append(c1);
                        resBuilder.Append(c0);
                        resBuilder.Append(paddingChar);
                    }
                    break;

                case 4:
                    {
                        buf = (((data[endOffset - 4] * (long)0x100 + data[endOffset - 3]) * (long)0x100 + data[endOffset - 2]) * (long)0x100 + data[endOffset - 1]) * (long)0x100;
                        c0 = charTable[(int)(buf % 1024 / 0x100 + 1024)]; buf /= 1024;
                        c1 = charTable[(int)(buf % 1024)]; buf /= 1024;
                        c2 = charTable[(int)(buf % 1024)]; buf /= 1024;
                        resBuilder.Append(charTable[(int)buf]);
                        resBuilder.Append(c2);
                        resBuilder.Append(c1);
                        resBuilder.Append(c0);
                    }
                    break;
            }

            return resBuilder.ToString();
        }


        /// <summary>
        /// Decode a BaseHangul encoded string to binary data
        /// </summary>
        /// <param name="encodedString">BaseHangul encoded string</param>
        /// <param name="isTrustInput">If you set this value to true when you can trust that <paramref name="encodedString"/> is a valid BaseHangul encoded string, this function will skip validation check for input string. Default value is false.</param>
        /// <returns>Binary data for given BaseHangul encoded string</returns>
        public static byte[] Decode(string encodedString, bool isTrustInput = false)
        {
            string charToDecode = encodedString;
            if (!isTrustInput)
            {
                PreparePattern();
                if (!Regex.IsMatch(charToDecode, checkPattern))
                {
                    charToDecode = Regex.Replace(charToDecode, filterPattern, string.Empty);
                }
            }
            charToDecode = charToDecode.TrimEnd(paddingChar);

            int decodeLength = charToDecode.Length;
            if (decodeLength == 0)
                return new byte[0];

            InitializeReverseTable();
            List<byte> buffer = new List<byte>(GetMaxDecodedLength(decodeLength));
            long buf;
            byte b0, b1, b2, b3;
            for (int i = 4; i < decodeLength; i += 4)
            {
                buf = ((reverseTable[encodedString[i - 4]] * (long)1024 + reverseTable[encodedString[i - 3]]) * (long)1024 + reverseTable[encodedString[i - 2]]) * (long)1024 + reverseTable[encodedString[i - 1]];
                b0 = (byte)(buf % 0x100); buf /= 0x100;
                b1 = (byte)(buf % 0x100); buf /= 0x100;
                b2 = (byte)(buf % 0x100); buf /= 0x100;
                b3 = (byte)(buf % 0x100); buf /= 0x100;
                buffer.Add((byte)(buf % 0x100));
                buffer.Add(b3);
                buffer.Add(b2);
                buffer.Add(b1);
                buffer.Add(b0);
            }
            switch (decodeLength % 4)
            {
                case 1:
                    {
                        buffer.Add((byte)(reverseTable[encodedString[decodeLength - 1]] / 4));
                    }
                    break;

                case 2:
                    {
                        buf = (reverseTable[encodedString[decodeLength - 2]] * (long)1024 + reverseTable[encodedString[decodeLength - 1]]) / 16;
                        b0 = (byte)(buf % 0x100); buf /= 0x100;
                        buffer.Add((byte)(buf % 0x100));
                        buffer.Add(b0);
                    }
                    break;

                case 3:
                    {
                        buf = ((reverseTable[encodedString[decodeLength - 3]] * (long)1024 + reverseTable[encodedString[decodeLength - 2]]) * (long)1024 + reverseTable[encodedString[decodeLength - 1]]) / 64;
                        b0 = (byte)(buf % 0x100); buf /= 0x100;
                        b1 = (byte)(buf % 0x100); buf /= 0x100;
                        buffer.Add((byte)(buf % 0x100));
                        buffer.Add(b1);
                        buffer.Add(b0);
                    }
                    break;

                case 0:
                    {
                        bool isSkipLast = false;
                        int lastCharValue = reverseTable[encodedString[decodeLength - 1]];
                        if (lastCharValue >= 1024)
                        {
                            buf = ((reverseTable[encodedString[decodeLength - 4]] * (long)1024 + reverseTable[encodedString[decodeLength - 3]]) * (long)1024 + reverseTable[encodedString[decodeLength - 2]]) * (long)1024 + ((lastCharValue - 1024) * (long)0x100);
                            isSkipLast = true;
                        }
                        else
                        {
                            buf = ((reverseTable[encodedString[decodeLength - 4]] * (long)1024 + reverseTable[encodedString[decodeLength - 3]]) * (long)1024 + reverseTable[encodedString[decodeLength - 2]]) * (long)1024 + lastCharValue;
                        }
                        b0 = (byte)(buf % 0x100); buf /= 0x100;
                        b1 = (byte)(buf % 0x100); buf /= 0x100;
                        b2 = (byte)(buf % 0x100); buf /= 0x100;
                        b3 = (byte)(buf % 0x100); buf /= 0x100;
                        buffer.Add((byte)(buf % 0x100));
                        buffer.Add(b3);
                        buffer.Add(b2);
                        buffer.Add(b1);
                        if (!isSkipLast)
                            buffer.Add(b0);
                    }
                    break;
            }

            return buffer.ToArray();
        }


        /// <summary>
        /// Calculate an expected BaseHangul encoded string length for data of given length
        /// </summary>
        /// <param name="binaryLength">Binary data length to encode</param>
        /// <returns>Expected encoded string length</returns>
        public static int GetEncodedLength(int binaryLength)
        {
            if (binaryLength <= 0)
                return 0;
            return ((binaryLength - 1) / 5) * 4 + 1;
        }

        /// <summary>
        /// Calculate a maximum length of binary data for a string of given length when decoded as BaseHangul
        /// </summary>
        /// <param name="encodedStringLength">BaseHangul encoded string length to decode</param>
        /// <returns>Maximum length of binary data for the given encoded string length</returns>
        public static int GetMaxDecodedLength(int encodedStringLength)
        {
            if (encodedStringLength <= 0)
                return 0;
            return ((encodedStringLength / 4) * 5) + encodedStringLength % 4;
        }
    }
}
